查看原文
其他

dma-buf学习分享

尹忠凯 Linux阅码场 2022-12-14


作者简介

尹忠凯, Linux内核爱好者,毕业杭州电子科技大学,现在就职于北京地平线信息技术有限公司,任系统软件工程师

1.dma-buf简介

dma-buf是kernel提供的一个框架,它主要是为了解决不同设备驱动之间buf共享的问题。

2.示例说明

"Talk is cheap. Show me the code.",单纯的文字描述比较抽象,我们通过实验先来看下dma-buf是怎么使用的。

2.1 用户空间

/* test.c */int main(void){ /* open dma-heap */ heap_fd = open("/dev/dma_heap/global_cma@68000000", O_RDWR);
data.len = 1024 * 1024; // 1M data.fd = 0; data.fd_flags = O_RDWR | O_CLOEXEC; data.heap_flags = 0;
/* alloc buf */ ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data); buf_fd = (int)data.fd; p = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, buf_fd, 0);
/* driver A */ fda = open("/dev/driverA", O_RDWR);
infoa.fd = buf_fd; infoa.buf = p; infoa.size = 1024 * 1024; // 1M strcpy((char *)infoa.buf, "driverA: userspace!");
ret = ioctl(fda, TEST_DRIVERA, &infoa); printf("driverA buf = %s\n", infoa.buf);
/* driver B */ fdb = open("/dev/driverB", O_RDWR);
infob.fd = buf_fd; infob.buf = p; infob.size = 1024 * 1024; // 1M ret = ioctl(fda, TEST_DRIVERB, &infob); printf("driverB buf = %s\n", infob.buf);
return 0;}


2.2 内核空间

/* test-driver.c */static long test_ioctl(struct file *file, unsigned cmd, unsigned long arg){ int i; struct buf_info info; struct scatterlist *sg;
if (copy_from_user(&info, (void __user *)arg, sizeof(info)) != 0) { printk("copy_from_user failed\n"); return -EFAULT; } printk("fd[%d], buf[0x%px], size[0x%x]\n", info.fd, info.buf, info.size);
switch(cmd) { case TEST_DRIVERA: /* for dma access */ test_dev.dma_buf = dma_buf_get(info.fd); test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device); test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);
for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) { printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n", __FUNCTION__, __LINE__, sg->dma_address, sg->length); }
dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE); dma_buf_detach(test_dev.dma_buf, test_dev.attach); dma_buf_put(test_dev.dma_buf);
/* for cpu access */ dma_buf_vmap(test_dev.dma_buf, &test_dev.map); printk("<%s: %d>addr = 0x%px, str = %s\n", __FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr); strcpy((char *)test_dev.map.vaddr, "driverA kernel space!"); dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);
break;
case TEST_DRIVERB: /* for dma access */ test_dev.dma_buf = dma_buf_get(info.fd); test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device); test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);
for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) { printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n", __FUNCTION__, __LINE__, sg->dma_address, sg->length); }
dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE); dma_buf_detach(test_dev.dma_buf, test_dev.attach); dma_buf_put(test_dev.dma_buf);
/* for cpu access */ dma_buf_vmap(test_dev.dma_buf, &test_dev.map); printk("<%s: %d>addr = 0x%px, str = %s\n", __FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr); strcpy((char *)test_dev.map.vaddr, "driverB kernel space!"); dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);
break; }
return 0;}


2.3 测试log


/ # /app/test[ 31.678467] fd[4], buf[0x0000ffffb25b9000], size[0x100000][ 31.684938] <test_ioctl: 48>addr = 0x68100000, len = 0x100000[ 31.688969] <test_ioctl: 57>addr = 0xffff800010ba5000, str = driverA: userspace!driverA buf = driverA kernel space![ 31.701777] fd[4], buf[0x0000ffffb25b9000], size[0x100000][ 31.703808] <test_ioctl: 70>addr = 0x68100000, len = 0x100000[ 31.704973] <test_ioctl: 79>addr = 0xffff800010ca6000, str = driverA kernel space!driverB buf = driverB kernel space!/ #


2.4 代码分析


结合上图及测试程序我们分析下dma-buf的使用流程。

  • 步骤1打开/dev/dma_heap/global_cma@68000000节点,该节点是kernel提供的dma-heap框架创建的一个节点。

  • 步骤2通过ioctl()global_cma中申请一块1M大小内存(global_cma是从dts中配置的,以0x68000000为起始地址,大小为128M的内存块),对于test程序来说,申请内存是以data.fd的形式体现,也就是buf_fd

  • 步骤3通过mmap()函数映射后,就可以对申请的内存进行使用了。

  • 步骤4,5打开/dev/driverA这个节点,通过ioctl()函数将infoa这个结构传递到内核空间。然后继续分析测试log。

    • test-drvier.c中(30 ~ 35行)代码,也是通过dma_buf_vmap()接口,可以将dma_buf转化为虚拟地址map.vaddr,这意味着我们就可以通过cpu来操作这块内存了

    • 另外打印出内存地址内容(测试log第4行),可以看出就是我们在用户空间往这个地址写的内容driverA: userspace!

    • 最后把内存内容修改,并返回到用户空间也可以看到内存空间已经被修改(测试log第5行)

    • 首先通过buf_fd可以获取dma_buf结构,然后通过dma_buf_attach()dma_buf_map_attachment(),函数将dma_buf转化为test_dev.sg结构

    • test_dev.sg结构就可以直接给到dma来使用了,另外打印test_dev.sg结构中的地址可以看出,物理地址是以0x68100000为基址,大小为1M的内存块,正好落在global_cma区域(测试log第3行打印)。

    • test-driver.c中(15 ~ 37行)分别测试了dma和cpu两种内存访问方式

    • dma访问内存方式(由于使用qemu环境进行实验,实际没有dma只是从地址打印来看)

    • cpu访问内存方式

  • 步骤5, 6打开/dev/driverB这个节点,通过ioctl()函数将infob这个结构传递到内核空间。然后继续分析测试log。

    • 该部分代码为test-driver.c中40 ~ 61行代码,也包含dma和cpu两种内存访问方式

    • 主要看下测试log第8行,打印内存内容为driverA kernel space!,也就是driverA中写入的内容,这说明同一块内存可以在不同的驱动之间进行共享

2.5 示例总结

分析完上面的示例程序,简单总结下

  • 首先通过/dev/dma_heap/global_cma@68000000节点来从global_cma来申请内存,该内存对用户空间来说是以fd的形式体现,经过mmap()函数映射后,我们就可以对这块内存进行操作了。

  • fd传递到内核空间后,驱动程序可以通过fd获取其所绑定的dma_buf结构,通过dma_buf结构,我们可以得到sg结构用于dma对内存访问,得到map.vaddr结构用于cpu对内存访问。

  • 另外buf可以在不同驱动之间进行共享,对用户空间buf体现为fd,对kernel空间buf体现为dma_buf结构,而kernel提供的dma-buf框架用于维护两者的关系从而实现不同驱动之间的buf共享。

2.6 示例扩展



上面的示例程序中只有一个buf,可以进一步扩展为多个buf

  • test程序把待处理的数据通过ioctl()接口put到driverA的链表上,当driverA处理完后,可以把处理好的的数据放到另一个链表上,然后通知上层来取(比如通过poll/select机制来通知上层)。

  • 上层test层拿到driverA处理的数据又可以通过ioctl()接口put到driverB的链表上,当driverB处理完后,又可以把处理好的数据放到另一个链表上,然后通知上层来取。

  • 当然可以有很多这样的驱动程序,前一级的输出结果是下一级的输入,这样一级一级处理,而无须数据在用户空间与内核空间不停的拷贝。

3.dma-buf框架

前面也提到了dma-buf框架是用来管理fd与dma_buf之前关系的,接下来我们介绍下dma-buf框架是怎么实现的。



3.1 exporterimporter

在dma-buf框架中,有两个概念exporterimporter,如上图蓝色虚线框为exporter,紫色虚线框为importer

  • exporter是buf的管理者,主要作用如下

    • 实现对buf的分配管理,比如,buf从那里分配,分配多少;这些是exporter实现的,像上面cma-heap从名字就可以知道,buf是从cma分配的,用户只需要打开/dev/dma_heap/global_cma@68000000这个节点申请内存就可以,具体内存怎么申请的,用户是无需关心的。

    • 实现对buf的操作,比如在用户空间想要操作这块buf需要先map一下,在内核空间相关通过dma或者cpu来访问该buf,或者做cache同步,这些都是由exporter来实现的,如图中左侧的cma_heap_buf_ops结构,就是该实现的一组操作函数。另外为什么需要由exporter来实现呢?因为exporter是清楚这些内存从那里申请的,因此当然清楚怎么map,所以由exporter来实现这些操作是再合理不过的。

  • importer是buf的使用者,对于buf的使用者来说就很简单了,只需要拿到buf的fd,然后通过dma-buf框架提供的接口,就可以对buf的数据进行操作了(前面示例程序也展示了)。

3.2 dma-heap框架

正常来说exporterimporter都需要我们自己实现的。比如在嵌入式系统中(有图像处理的场景中),一般需要划分出一段内存区域,用于图像数据的处理,对于该段内存的管理可以使用kernel现有的机制比如cma、保留内存等,当然我们也可以实现一个exporter而将内存管理起来;而对于importer往往就是内存的一使用者,一般来说就是我们自己写的驱动程序,比如上面的driverA,driverB测试程序。

kernel提供的dma-heap的框架,就是一个exporter,其实也非常简单,主要包含下面三个源文件。


drivers/dma-buf/dma-heap.c # 实现dma-heap的框架drivers/dma-buf/heaps/cma_heap.c # 管理从cma申请的内存drivers/dma-buf/heaps/system_heap.c # 管理从system申请的内存

  • dam-heap.c是一个抽象的接口层,为具体的heap(cma/system)提供注册机制,创建字符设备节点

  • cam_heap.c用于管理从cma申请的内存

  • system_heap.c用于管理从system申请的内存

dma-heap.c

/* drivers/dma-buf/dma-heap.c */ struct dma_heap *dma_heap_add(const struct dma_heap_export_info *exp_info){... heap = kzalloc(sizeof(*heap), GFP_KERNEL); if (!heap) return ERR_PTR(-ENOMEM);
heap->name = exp_info->name; heap->ops = exp_info->ops; heap->priv = exp_info->priv;
/* Find unused minor number */ ret = xa_alloc(&dma_heap_minors, &minor, heap, XA_LIMIT(0, NUM_HEAP_MINORS - 1), GFP_KERNEL); if (ret < 0) { pr_err("dma_heap: Unable to get minor number for heap\n"); err_ret = ERR_PTR(ret); goto err0; }
/* Create device */ heap->heap_devt = MKDEV(MAJOR(dma_heap_devt), minor);
cdev_init(&heap->heap_cdev, &dma_heap_fops); ret = cdev_add(&heap->heap_cdev, heap->heap_devt, 1);
dev_ret = device_create(dma_heap_class, NULL, heap->heap_devt, NULL, heap->name);
... return heap;}
static long dma_heap_ioctl(struct file *file, unsigned int ucmd, unsigned long arg){... switch (kcmd) { case DMA_HEAP_IOCTL_ALLOC: ret = dma_heap_ioctl_allocate(file, kdata); break; default: ret = -ENOTTY; goto err; }...}


dma-heap.c中提供的两个主要函数dma_heap_add()dma_heap_ioctl(),像cma_heapsystemp_heap会通过dma_heap_add()函数注册到dma-heap框架来,dma-heap会分别为他们创建字符设备节点供用户空间使用,并且提供一个通用的dma_heap_fops(24行代码)。通用的dma_heap_fops中也简单的提供了一个ioctl函数,并且只有一个命令DMA_HEAP_IOCTL_ALLOC(如上dma_heap_ioctl()函数),当然dam-heap具体是不清楚对应的heap就从那里申请内存,怎么申请的,它只是通过回调函数走到对应的heap中(简单粗暴)。

cma_heap.c

/* drivers/dma-buf/heaps/cma_heap.c */static const struct dma_buf_ops cma_heap_buf_ops = { .attach = cma_heap_attach, .detach = cma_heap_detach, .map_dma_buf = cma_heap_map_dma_buf, .unmap_dma_buf = cma_heap_unmap_dma_buf, .begin_cpu_access = cma_heap_dma_buf_begin_cpu_access, .end_cpu_access = cma_heap_dma_buf_end_cpu_access, .mmap = cma_heap_mmap, .vmap = cma_heap_vmap, .vunmap = cma_heap_vunmap, .release = cma_heap_dma_buf_release,};
static struct dma_buf *cma_heap_allocate(struct dma_heap *heap, unsigned long len, unsigned long fd_flags, unsigned long heap_flags){... cma_pages = cma_alloc(cma_heap->cma, pagecount, align, false); if (!cma_pages) goto free_buffer;....
/* create the dmabuf */ exp_info.ops = &cma_heap_buf_ops; exp_info.size = buffer->len; exp_info.flags = fd_flags; exp_info.priv = buffer; dmabuf = dma_buf_export(&exp_info); if (IS_ERR(dmabuf)) { ret = PTR_ERR(dmabuf); goto free_pages; } return dmabuf;...}

书接上文,当用户想要申请一块内存时,通过dma-heap的中转走到cma_heap.ccma_heap_allocate()函数中,可以看到申请是从cma申请的(第21行);关键看27 ~ 31行代码,通过dma_buf_export()函数就把该内存放到dma-buf框架管理起来了。

另外需要关注cma_heap_buf_ops操作函数集(第27行),对于该组函数从名字来看是不是很熟悉,当使用dma-buf框架提供的map函数时,dma-buf也不知道对应的内存该怎样map,最清楚的还是exporter本身,所以定义了一组回调函数,由exporter来自己来实现(简单粗暴)。

小结

大家可能都知道androidion,其实它和这里的dma-heap是处于相同地位的,只不过ion的实现更复杂。它除了可以在用户空间申请内存外,内核空间也提供了相应的接口,另外它还有一套自己的buf管理机制以及对不同类型buf管理的支持,但底层都是基于dma-buf来实现的。

3.3 dma-buf框架

前面说了这么多,终于来到了dma-buf框架,但实际dma-buf框架也是相当简单的,主要是学习它这种解决问题的思路。

dma_buf_export()函数

/* driver/dma-buf/dma-buf.c */struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info){...
dmabuf = kzalloc(alloc_size, GFP_KERNEL); if (!dmabuf) { ret = -ENOMEM; goto err_module; }
dmabuf->priv = exp_info->priv; dmabuf->ops = exp_info->ops; dmabuf->size = exp_info->size; dmabuf->exp_name = exp_info->exp_name; dmabuf->owner = exp_info->owner; spin_lock_init(&dmabuf->name_lock); init_waitqueue_head(&dmabuf->poll); dmabuf->cb_excl.poll = dmabuf->cb_shared.poll = &dmabuf->poll; dmabuf->cb_excl.active = dmabuf->cb_shared.active = 0;
if (!resv) { resv = (struct dma_resv *)&dmabuf[1]; dma_resv_init(resv); } dmabuf->resv = resv;
file = dma_buf_getfile(dmabuf, exp_info->flags); if (IS_ERR(file)) { ret = PTR_ERR(file); goto err_dmabuf; }
file->f_mode |= FMODE_LSEEK; dmabuf->file = file;
mutex_lock(&db_list.lock); list_add(&dmabuf->list_node, &db_list.head); mutex_unlock(&db_list.lock);
return dmabuf;}


前面有提到,对用户空间buf体现为fd,对kernel空间buf体现为dma_buf结构,dma-buf框架用于维护两者的关系,从上面代码可以看到,buf信息首先被包装到一个dma_buf结构中(第12 ~ 26行),然后通过dma_buf_getfile()函数将buf与fd绑定(第28 ~ 35行),最后将dma_buf结构放到一个链表维护起来(38行),是不是相当的简单。


dma_buf_getfile()函数

/* driver/dma-buf/dma-buf.c */static const struct file_operations dma_buf_fops = { .release = dma_buf_file_release, .mmap = dma_buf_mmap_internal, .llseek = dma_buf_llseek, .poll = dma_buf_poll, .unlocked_ioctl = dma_buf_ioctl, .compat_ioctl = compat_ptr_ioctl, .show_fdinfo = dma_buf_show_fdinfo,};
static struct file *dma_buf_getfile(struct dma_buf *dmabuf, int flags){ struct inode *inode = alloc_anon_inode(dma_buf_mnt->mnt_sb);
inode->i_size = dmabuf->size; inode_set_bytes(inode, dmabuf->size);
file = alloc_file_pseudo(inode, dma_buf_mnt, "dmabuf", flags, &dma_buf_fops); if (IS_ERR(file)) goto err_alloc_file; file->f_flags = flags & (O_ACCMODE | O_NONBLOCK); file->private_data = dmabuf; file->f_path.dentry->d_fsdata = dmabuf;
return file;}


再来看一眼dma_buf_getfile()函数,会通过系统申请一个file结构,并为该file绑定一组操作函数集dma_buf_fops,还记得前面test测试程序中,有一步map操作吧,该操作函数集也实现了mmap()函数,我们可以想象mmap()的实现方式,对于dma-buf框架来说,它是不清楚要怎么对内存map的,所以需要exporter自己来实现,如下为dma_buf_mmap_internal()函数实现。


/* driver/dma-buf/dma-buf.c */static int dma_buf_mmap_internal(struct file *file, struct vm_area_struct *vma){ struct dma_buf *dmabuf;
dmabuf = file->private_data;
/* check if buffer supports mmap */ if (!dmabuf->ops->mmap) return -EINVAL;
return dmabuf->ops->mmap(dmabuf, vma);}


dma-buf框架直接通过回调函数调到exporter中(第12行),也这印证了我们前面的猜想(虽然事先看过代码😆)  ,这里的mmap()函数是提供给用户空间使用的,像内核空间的dma_buf_attach()dma_buf_map_attachment()dma_buf_vmap()等函数,也都是这种回调的实现方式。

小结

经过前面的分析,dma-buf框架还是非常简单的,每一个buf都被包装成一个dma_buf结构,然后每一个dma_buf结构又会绑定一个file结构,最后将所有的dma_buf放到链表上维护,这样在内核空间通过遍历该链表就可以由fd到找对应的dma_buf结构。另外每一个file结构还会绑定一组通用的函数操作集,然后通过回调函数的方式,调到对应的exporter中,这是给用户空间提供的接口;内核空间的dma_buf_xxx()接口也都采用了正常的实现方式。

参考

  • https://blog.csdn.net/hexiaolong2009/article/details/102596744

  • https://www.cnblogs.com/yaongtime/p/14594567.html

  • https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#



如果你觉得你现在走得辛苦,那就证明你在走上坡路。🔥


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存